<script setup lang="ts">
import { ref, onMounted } from 'vue'
import p5 from 'p5'
import Victor from 'victor'

const canvasContainer = ref(null)

// 配置参数 - 只需修改这里就能调整链条
const numPoints = 100 // 链条中的点数（包括锚点）
const constraintDistance = 10 // 点与点之间的约束距离
const pointSize = 5 // 点的大小
const lineColor = [100, 100, 100] // 连接线颜色
const thicknessList = initSnakeThicknessList(20, 10, numPoints)
const minAngel = Math.PI / 1.1

// 使用数组存储所有点
const points = ref<Victor[]>([])

// 初始化点数组
const initializePoints = () => {
  points.value = []
  for (let i = 0; i < numPoints; i++) {
    points.value.push(new Victor(400, 300))
  }
}

// 初始化点
initializePoints()

// 使用 Victor.js 的约束函数
function constrainDistance(pointVec: Victor, anchorVec: Victor, distance: number): Victor {
  const displacement = pointVec.clone().subtract(anchorVec)
  const currentDistance = displacement.length()

  if (currentDistance != distance) {
    return anchorVec.clone().add(displacement.normalize().multiplyScalar(distance))
  }
  return pointVec.clone()
}

/**
 * 要求输入旧的M点，旧的E点，新的S点和约束角度。
 * 输出新的E点坐标
 * 要求新的E点坐标，满足将向量MS和向量ME形成的夹角，始终约束在大于给定angle的角度
 * @param S 起始点S
 * @param M 中点M
 * @param E 结束点E
 * @param minAngle 最小角度
 */
function constrainAngle(S: Victor, M: Victor, E: Victor, minAngle: number): Victor {
  // 计算向量 MS 和 ME
  const vectorMS = S.clone().subtract(M)
  const vectorME = E.clone().subtract(M)

  // 如果任一向量长度为零，无法计算角度，返回原E点
  if (vectorMS.length() === 0 || vectorME.length() === 0) {
    return E.clone()
  }

  // 计算当前夹角（弧度）
  const currentAngle = Math.abs(vectorMS.angle() - vectorME.angle())
  const normalizedAngle = Math.min(currentAngle, Math.PI * 2 - currentAngle)

  // 如果当前夹角已经大于等于约束角度，不需要调整
  if (normalizedAngle >= minAngle) {
    return E.clone()
  }

  // 计算需要旋转的角度
  const rotationNeeded = minAngle - normalizedAngle

  // 确定旋转方向（选择需要旋转最少的角度）
  const crossProduct = vectorMS.x * vectorME.y - vectorMS.y * vectorME.x
  const rotationDirection = crossProduct >= 0 ? 1 : -1

  // 对向量ME进行旋转
  const rotatedME = vectorME.rotate(rotationDirection * rotationNeeded)

  // 返回新的E点坐标
  return M.clone().add(rotatedME)
}

onMounted(() => {
  if (!canvasContainer.value) return

  const sketch = (p: p5) => {
    p.setup = () => {
      p.createCanvas(800, 600)
    }

    p.draw = () => {
      p.background(240)

      // 1. 第一个点（锚点）直接跟随鼠标
      points.value[0].x = p.mouseX
      points.value[0].y = p.mouseY

      // 2. 对链中的每个点应用约束
      for (let i = 1; i < numPoints; i++) {
        // 然后应用距离约束到前一个点
        points.value[i] = constrainDistance(
          points.value[i],
          points.value[i - 1],
          constraintDistance,
        )
        if (i > 1) {
          points.value[i] = constrainAngle(
            points.value[i - 2],
            points.value[i - 1],
            points.value[i],
            minAngel,
          )
        }
      }

      // 3. 绘制链条
      drawChain(p)

      // 4. 显示信息
      p.fill(0)
      p.noStroke()
      p.textSize(14)
      p.textAlign(p.LEFT)
      p.text(`链条点数: ${numPoints} | 约束距离: ${constraintDistance}`, 10, 20)
      p.text(`点击重置链条`, 10, 40)
    }

    /**
     * 计算左右边界连线点
     */
    const calculateBoundaryPoints = () => {
      const boundaryPoints: { left: Victor[]; right: Victor[] } = {
        left: [],
        right: [],
      }
      for (let i = 0; i < numPoints - 1; i++) {
        const point1 = points.value[i]
        const point2 = points.value[i + 1]
        if (point1 && point2) {
          // 算出“向前”的向量（即这一节身体的向量）
          const vectorForward = point1?.clone().subtract(point2)
          // 将此向量，向两侧旋转各90度得到两个新的向量
          const vectorSE90 = vectorForward
            .clone()
            .rotate(Math.PI / 2)
            .normalize()
            .multiplyScalar(thicknessList[i])
          const vectorSE90_2 = vectorForward
            .clone()
            .rotate(-Math.PI / 2)
            .normalize()
            .multiplyScalar(thicknessList[i])
          boundaryPoints.left.push(point1.clone().add(vectorSE90_2))
          boundaryPoints.right.push(point1.clone().add(vectorSE90))
        }
      }
      return boundaryPoints
    }

    // 绘制链条函数
    const drawChain = (p: p5) => {
      const boundaryPoints = calculateBoundaryPoints()
      // 绘制所有连接线
      for (let i = 0; i < numPoints - 1; i++) {
        p.stroke(lineColor[0], lineColor[1], lineColor[2])
        p.strokeWeight(2)

        if (i == 0 || i == numPoints - 2) {
          p.line(
            boundaryPoints.left[i].x,
            boundaryPoints.left[i].y,
            boundaryPoints.right[i].x,
            boundaryPoints.right[i].y,
          )
        }

        // 绘制脊椎连接线
        p.line(points.value[i].x, points.value[i].y, points.value[i + 1].x, points.value[i + 1].y)

        if (boundaryPoints.left[i] && boundaryPoints.left[i + 1]) {
          p.line(
            boundaryPoints.left[i].x,
            boundaryPoints.left[i].y,
            boundaryPoints.left[i + 1].x,
            boundaryPoints.left[i + 1].y,
          )
        }
        if (boundaryPoints.right[i] && boundaryPoints.right[i + 1]) {
          p.line(
            boundaryPoints.right[i].x,
            boundaryPoints.right[i].y,
            boundaryPoints.right[i + 1].x,
            boundaryPoints.right[i + 1].y,
          )
        }
      }

      // 绘制所有点（使用渐变颜色和大小）
      for (let i = 0; i < numPoints; i++) {
        const size = pointSize
        const hue = 360

        // 绘制点
        p.fill(hue, 70, 60)
        p.noStroke()
        p.ellipse(points.value[i].x, points.value[i].y, size)
      }
    }

    p.mousePressed = () => {
      // 点击时重置链条位置
      initializePoints()
    }
  }

  new p5(sketch, canvasContainer.value)
})

/**
 * 生成一个按步长递增的数组
 * @param startValue 起始值
 * @param endValue 结尾值
 * @param count 数组长度
 * @returns 数组
 */
function generateSteppedArray(startValue: number, endValue: number, count: number): number[] {
  if (count <= 0) {
    throw new Error('数组长度必须大于0')
  }
  // 特殊情况：如果长度为1，直接返回起始值或结尾值
  if (count === 1) {
    return [startValue]
  }
  // 计算步长
  const step = (startValue - endValue) / (count - 1)
  // 生成数组
  const result: number[] = []
  for (let i = 0; i < count; i++) {
    result.push(startValue - i * step)
  }
  return result
}

/**
 * 生成一个先慢后快的递增数组
 * @param startValue 起始值
 * @param endValue 结尾值
 * @param count 数组长度
 * @param exponent 加速因子（默认值为 2）
 * @returns 先慢后快的递增数组
 */
function generateAcceleratingArray(
  startValue: number,
  endValue: number,
  count: number,
  exponent: number = 2,
): number[] {
  if (count <= 0) {
    throw new Error('数组长度必须大于0')
  }
  if (count === 1) {
    return [startValue]
  }

  const result: number[] = []
  for (let i = 0; i < count; i++) {
    const t = i / (count - 1) // 归一化到 [0, 1]
    const acceleratedT = Math.pow(t, exponent) // 加速效果
    const value = startValue + (endValue - startValue) * acceleratedT
    result.push(value)
  }

  return result
}
/**
 * 初始化蛇的粗细数组
 * @param startValue
 * @param endValue
 * @param count
 */
/**
 * 初始化蛇的粗细数组
 * @param startValue 起始值
 * @param endValue 结尾值
 * @param count 数组长度
 */
/**
 * 初始化蛇的粗细数组
 * @param startValue 起始值
 * @param endValue 结尾值
 * @param count 数组长度
 */
function initSnakeThicknessList(startValue: number, endValue: number, count: number) {
  const snake = generateSteppedArray(startValue, endValue, count)

  // 计算头部、颈部和尾部的长度，确保为整数
  const headNumber = Math.floor(count / 15)
  const neckNumber = Math.floor(count / 20)
  const tailNumber = Math.floor(count / 10)

  // 生成头部、颈部和尾部的数组
  const head = generateAcceleratingArray(1, 25, headNumber, 0.5)
  const neck = generateAcceleratingArray(25, 20, neckNumber, 1)
  const tail = generateAcceleratingArray(10, 1, tailNumber, 1)

  // 替换头部部分
  snake.splice(0, headNumber, ...head)

  // 替换颈部部分，避免越界
  const neckStartIndex = headNumber
  const neckDeleteCount = Math.min(neckNumber, snake.length - neckStartIndex)
  snake.splice(neckStartIndex, neckDeleteCount, ...neck)

  // 替换尾部部分，避免越界
  const tailStartIndex = count - tailNumber
  const tailDeleteCount = Math.min(tailNumber, snake.length - tailStartIndex)
  snake.splice(tailStartIndex, tailDeleteCount, ...tail)

  return snake
}
</script>

<template>
  <div ref="canvasContainer" class="canvas-container">
    <div class="config-panel">
      <h3>链条配置</h3>
      <p>点数: {{ numPoints }} | 距离: {{ constraintDistance }}</p>
      <p>移动鼠标拖动 | 点击重置</p>
    </div>
  </div>
</template>

<style scoped>
.canvas-container {
  position: relative;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
  border: 2px solid #e0e0e0;
}

.config-panel {
  position: absolute;
  top: 10px;
  left: 10px;
  background: rgba(255, 255, 255, 0.9);
  padding: 10px;
  border-radius: 8px;
  font-size: 12px;
}

.config-panel h3 {
  margin: 0 0 5px 0;
  font-size: 14px;
}

.config-panel p {
  margin: 2px 0;
}
</style>
